package com.googlecode.afdbaccess.buildtool.ant;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Properties;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;

/**
 * Use this task like this:
 * <p/>
 * <taskdef name="FileTransform"
 * classname="com.googlecode.afdbaccess.build.ant.FileTransform"
 * classpath="${build.dir}/tools/classes"/>
 * <p/>
 * <FileTransform todir="${build.dir}/intermediate/js">
 * <fileset dir="${build.dir}/intermediate/js">
 * <include name="*.js"/>
 * </fileset>
 * </FileTransform>
 * <p/>
 * This task will copy the files specified in the nested fileset, will rename them
 * (add checksum to the filename)
 * and will set an ant property which name is the name of the original file, and
 * the value is the full name of the destination file.
 */
public class FileTransform extends Task {
    private String todir;
    private boolean changefilenames;
    private String propertiesfile;
    private ArrayList<FileSet> filesets = new ArrayList<FileSet>();

    public void setTodir(String todir) {
        this.todir = todir;
    }

    public void setChangefilenames(boolean changefilenames) {
        this.changefilenames = changefilenames;
    }

    public void setPropertiesfile(String propertiesfile) {
        this.propertiesfile = propertiesfile;
    }

    public void addFileset(FileSet fileset) {
        filesets.add(fileset);
    }

    protected void validate() {
        if (todir == null)
            throw new BuildException("attribute \"todir\" not set");
        File dest = new File(todir);
        if (!dest.exists() || !dest.isDirectory())
            throw new BuildException("the value specified by the attribute \"todir\" is not valid");
        if (filesets.size() < 1)
            throw new BuildException("fileset not set");
    }

    @Override
    public void execute() {
        validate();
        
        Properties properties = new Properties();
        
        copyFilesToChecksummedFilenames(properties);

        savePropertiesFile(properties);
    }
    
    //

    private void copyFilesToChecksummedFilenames(Properties properties) {
        for (FileSet fs : filesets) {
            DirectoryScanner ds = fs.getDirectoryScanner(getProject());

            for (String includedFile : ds.getIncludedFiles()) {
                String sourceFilename = includedFile.replace('\\', '/');
                File sourceFile = new File(ds.getBasedir(), includedFile);

                String destinationFilename = getDestinationFilename(sourceFile, sourceFilename);

                createDirAndCopyFile(sourceFile, destinationFilename);

                // Save the name mapping in property
                String propertyValue = destinationFilename;
                getProject().setNewProperty(sourceFilename, propertyValue);
                properties.setProperty(sourceFilename, propertyValue);
            }
        }
    }
    
    //
    
    private void savePropertiesFile(Properties properties) {
        if (propertiesfile != null) {
            try {
                properties.store(new FileOutputStream(new File(propertiesfile)), null);
            } catch (IOException e) {
                e.printStackTrace();
                throw new BuildException("Unable to create properties file", e);
            }
        }
    }
    
    //

    private String getDestinationFilename(File srcFile, String srcFilename) {
        if (changefilenames) {
            String checksum = calculateChecksum(srcFile);
            String dstFilename = addStringToFilename(srcFilename, checksum);

            System.out.println("Filename: " + dstFilename);
            
            return dstFilename;
        } else {
            return srcFilename;
        }
    }

    private String addStringToFilename(String srcFilename, String toAdd) {
        int indexOfLastSlash = srcFilename.lastIndexOf('/');
        int indexOfLastPeriod = srcFilename.lastIndexOf('.');
        
        if(indexOfLastSlash >= 0 && indexOfLastPeriod < indexOfLastSlash) {
            // found period is before the last slash, so it is in a directoryname, not in the file name
            indexOfLastPeriod = -1;
        }
        
        String extension = "";
        if (indexOfLastPeriod != -1) {
            extension = srcFilename.substring(indexOfLastPeriod);
        }
        
        String base = srcFilename.substring(0, srcFilename.length()-extension.length());
        
        return base + "-" + toAdd + extension;
    }

    private String calculateChecksum(File srcFile) {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            DigestInputStream dis = new DigestInputStream(new FileInputStream(srcFile), messageDigest);
            
            byte[] tempBuf = new byte[128];
            while (dis.read(tempBuf) >= 0) {
                // do nothing
            }

            return toHexString(messageDigest.digest());
        } catch (IOException ioe) {
            ioe.printStackTrace();
            throw new BuildException("Unable to calculate the checksum", ioe);
        } catch (NoSuchAlgorithmException nsae) {
            throw new BuildException("Unable to get MD5 instance", nsae);
        }
    }
    
    //
    
    public static String toHexString(final byte[] bytes) {
        final char[] hexArray = { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' };
        char[] result = new char[bytes.length * 2];

        for (int bytesCounter = 0; bytesCounter < bytes.length; bytesCounter++) {
            int currentValue = bytes[bytesCounter] & 0xFF;
            result[bytesCounter*2] = hexArray[currentValue/16];
            result[bytesCounter*2 + 1] = hexArray[currentValue%16];
        }
        
        return new String(result);
    }

    //
    
    private void createDirAndCopyFile(File srcFile, String dstFilename) {
        File dstFile = new File(todir, dstFilename);
        dstFile.getParentFile().mkdirs();

        try {
            copy(srcFile, dstFile);
        } catch (IOException e) {
            e.printStackTrace();
            throw new BuildException("Unable to copy the file", e);
        }
    }

    private static void copy(File source, File dest) throws IOException {
        FileChannel in = null, out = null;
        try {
            in = new FileInputStream(source).getChannel();
            out = new FileOutputStream(dest).getChannel();
            long size = in.size();
            MappedByteBuffer buf = in.map(FileChannel.MapMode.READ_ONLY, 0, size);
            out.write(buf);
        } finally {
            if (in != null) in.close();
            if (out != null) out.close();
        }
    }
}
